// Copyright 2011, Vanya Davidenko.
// Используемая кодировка: UTF-8.
 
#ifndef CHVI_MOUSE_CONTROL_H_
#define CHVI_MOUSE_CONTROL_H_

#ifndef _WIN32
# error "This header should be used in Windows only"
#endif 

#include <windows.h>
#include <assert.h>
#include <stdexcept>



namespace cvhi {

/// Исключение, генерируемое методами класса MouseControl.
class MouseControlException : public ::std::exception {
  public:
    MouseControlException(const ::std::string& msg) 
        : ::std::exception(msg.c_str()) {}
};



// Управление мышью в Windows.
class MouseControl {
  public:
    enum Button { LEFT, MIDDLE, RIGHT };
    enum Scroll { VERTICAL, HORIZONTAL };

    /// Изменения координаты в пикселах.
    /// @param[in] dx изменение координаты x
    /// @param[in] dy изменение координаты y.
    /// @except MouseControlException.
    static void move(const int dx, const int dy) {
        INPUT input = defaultInput();
        input.mi.dx = dx;
        input.mi.dy = dy;
        input.mi.dwFlags = MOUSEEVENTF_MOVE;
        send(&input);
    }

    /// Задание абсолютных позиции.
    /// @param[in] x_percent процент от ширины экрана  
    /// @param[in] y_percent процент от высоты экрана 
    /// @param[in] use_whole_virtual_desktop использовать площадь всего
    ///            виртуального рабочего стола (true) или только основного
    ///            монитора (false).
    /// @except MouseControlException.
    static void setPosition(const char x_percent, const char y_percent,
                            const bool use_whole_virtual_desktop = true) {
        INPUT input = defaultInput();
        input.mi.dx = x_percent * 65535 / 100;
        input.mi.dy =  y_percent * 65535 / 100;
        input.mi.dwFlags = MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_MOVE;
        if ( use_whole_virtual_desktop ) {
            input.mi.dwFlags |= MOUSEEVENTF_VIRTUALDESK;
        }
        send(&input);
    }

    /// Задание координат курсора.
    /// @param[in] x абсолютное значение по горизонтали в пикселах
    /// @param[in] y абсолютное значение по вертикали в пикселах.
    /// @except MouseControlException.
    static void setCoords(const int x, const int y) {
        if ( ::SetCursorPos(x,y) == FALSE ) {
            throw MouseControlException("SetCursorPos failed");
        }
    }

    /// Получение координат текущего положения курсора.
    /// @return структура с абсолютными координатами в полях x и y.
    /// @except MouseControlException.
    static POINT getCoords() {
        POINT result = { 0 };
        if ( ::GetCursorPos(&result) == FALSE ) {
            throw MouseControlException("GetCursorPos failed");
        }
        return result;
    }

    /// Поворот скролла.
    /// @param[in] scroll тип скролла (вертикальный/горизонтальный)
    /// @param[in] num_rotations число оборотов,
    ///            положительное число — поворот вперед/вправо.
    /// @warning Горизонтальный скролл поддерживается начиная с Windows Vista.
    /// @except MouseControlException.
    static void scroll(const Scroll scroll, const double num_rotatations) {                                                                                                                                                                                                                                                                                                                                                                                                                             
        INPUT input = defaultInput();
        switch ( scroll ) {
            case VERTICAL: input.mi.dwFlags = MOUSEEVENTF_WHEEL; break;
            case HORIZONTAL: input.mi.dwFlags = MOUSEEVENTF_HWHEEL; break;
            default: assert(false);
        }
        input.mi.mouseData = static_cast<DWORD>(WHEEL_DELTA * num_rotatations);
        send(&input);
    }

    /// Нажатие на кнопку.
    /// @param[in] button тип кнопки (левая/центральная/правая).
    /// @except MouseControlException.
    static void buttonDown(const Button button) {
        INPUT input = defaultInput();
        switch ( button ) {
            case LEFT: input.mi.dwFlags = MOUSEEVENTF_LEFTDOWN; break;
            case MIDDLE: input.mi.dwFlags = MOUSEEVENTF_MIDDLEDOWN; break;
            case RIGHT: input.mi.dwFlags = MOUSEEVENTF_RIGHTDOWN; break;
            default: assert(false);
        }
        send(&input);
    }

    /// "Отпускание" кнопки.
    /// @param[in] button тип кнопки (левая/центральная/правая).
    /// @except MouseControlException.
    static void buttonUp(const Button button) {
        INPUT input = defaultInput();
        switch ( button ) {
            case LEFT: input.mi.dwFlags = MOUSEEVENTF_LEFTUP; break;
            case MIDDLE: input.mi.dwFlags = MOUSEEVENTF_MIDDLEUP; break;
            case RIGHT: input.mi.dwFlags = MOUSEEVENTF_RIGHTUP; break;
            default: assert(false);
        }
        send(&input);
    }

    /// Клик кнопкой.
    /// @param[in] button тип кнопки (левая/центральная/правая).
    /// @except MouseControlException.
    static void buttonClick(const Button button) {
        /// TODO: заменить на двойное задание через один SendInput.
        buttonDown(button);
        buttonUp(button);
    } 


  private:
    static INPUT defaultInput() {
        const MOUSEINPUT mouse_input = { 0 };

        INPUT input = { 0 };
        input.type = INPUT_MOUSE;
        input.mi = mouse_input;

        return input;
    }

    /// TODO: добавить libv::OsErrCode2StringMsg
    static void send(INPUT* input) {
        if ( ::SendInput(1, input, sizeof(INPUT)) == FALSE ) {
            throw MouseControlException("SendInput failed");
        }
    }

    MouseControl() {}
};

}  // cvhi

#endif  /// CHVI_MOUSE_CONTROL_H_
